package net.esj.basic.dao.jdbc.jpa.support;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;

import net.esj.basic.dao.jdbc.jpa.Column;
import net.esj.basic.dao.jdbc.jpa.ResultSetExtractor;
import net.esj.basic.dao.jdbc.jpa.TableBeanMapping;

public class JPABeanResultSetExtractor implements ResultSetExtractor {
	Class entityType;
	TableBeanMapping mapping;

	public JPABeanResultSetExtractor(Class entityType, TableBeanMapping mapping) {
		this.entityType = entityType;
		this.mapping = mapping;
	}

	/**
	 * 假设bean的名称和数据库里字段名相同，并且没有多余的。
	 */
	public Object extractData(ResultSet rs) {
		Object bean;
		try {
			bean = entityType.newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
		Column[] cols = mapping.getColumns();
		PropertyDescriptor[] pds = mapping.getPropertyDescriptors();
		try {
			java.sql.ResultSetMetaData metaData;
			int columnCount;
			metaData = rs.getMetaData();
			columnCount = metaData.getColumnCount();
		for (int i = 0; i < columnCount; i++) {
			String colName = metaData.getColumnName(i + 1);
			int j = mapping.getIndexOfColName(colName);
			if(j < 0)continue;
			PropertyDescriptor pd = pds[j];
			Class propType = pd.getPropertyType();
			Object value;
				value = this.processColumn(rs, i + 1, propType);
				if (propType != null && value == null && propType.isPrimitive()) {
					value = primitiveDefaults.get(propType);
				}
				callSetter(bean, pd, value);

		}
		} catch (SQLException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
		return bean;
	}

	protected Object processColumn(ResultSet rs, int index, Class propType)
			throws SQLException {

		if (propType.equals(String.class)) {
			return rs.getString(index);

		} else if (propType.equals(Integer.TYPE)
				|| propType.equals(Integer.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if(decimal == null)return null;
			return decimal.intValue();

		} else if (propType.equals(Boolean.TYPE)
				|| propType.equals(Boolean.class)) {
			return new Boolean(rs.getBoolean(index));

		} else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if(decimal == null)return null;
			return decimal.longValue();

		} else if (propType.equals(Double.TYPE)
				|| propType.equals(Double.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if(decimal == null)return null;
			return decimal.doubleValue();

		} else if (propType.equals(Float.TYPE) || propType.equals(Float.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if(decimal == null)return null;
			return decimal.floatValue();

		} else if (propType.equals(Short.TYPE) || propType.equals(Short.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if(decimal == null)return null;
			return decimal.shortValue();

		} else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
			return new Byte(rs.getByte(index));

		} else if (propType.equals(Timestamp.class)) {
			return rs.getTimestamp(index);

		} else {
			return rs.getObject(index);
		}

	}

	private void callSetter(Object target, PropertyDescriptor prop, Object value)
			throws SQLException {

		Method setter = prop.getWriteMethod();

		if (setter == null) {
			return;
		}

		Class[] params = setter.getParameterTypes();
		try {
			// convert types for some popular ones
			if (value != null) {
				if (value instanceof java.util.Date) {
					if (params[0].getName().equals("java.sql.Date")) {
						value = new java.sql.Date(((java.util.Date) value)
								.getTime());
					} else if (params[0].getName().equals("java.sql.Time")) {
						value = new java.sql.Time(((java.util.Date) value)
								.getTime());
					} else if (params[0].getName().equals("java.sql.Timestamp")) {
						value = new java.sql.Timestamp(((java.util.Date) value)
								.getTime());
					}
				}
			}

			// Don't call setter if the value object isn't the right type
			if (this.isCompatibleType(value, params[0])) {
				setter.invoke(target, new Object[] { value });
			} else {
				throw new SQLException("Cannot set " + prop.getName()
						+ ": incompatible types.");
			}

		} catch (IllegalArgumentException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());

		} catch (IllegalAccessException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());

		} catch (InvocationTargetException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());
		}
	}

	/**
	 * ResultSet.getObject() returns an Integer object for an INT column. The
	 * setter method for the property might take an Integer or a primitive int.
	 * This method returns true if the value can be successfully passed into the
	 * setter method. Remember, Method.invoke() handles the unwrapping of
	 * Integer into an int.
	 * 
	 * @param value
	 *            The value to be passed into the setter method.
	 * @param type
	 *            The setter's parameter type.
	 * @return boolean True if the value is compatible.
	 */
	private boolean isCompatibleType(Object value, Class type) {
		// Do object check first, then primitives
		if (value == null || type.isInstance(value)) {
			return true;

		} else if (type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
			return true;

		} else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) {
			return true;

		} else if (type.equals(Double.TYPE) && Double.class.isInstance(value)) {
			return true;

		} else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) {
			return true;

		} else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) {
			return true;

		} else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
			return true;

		} else if (type.equals(Character.TYPE)
				&& Character.class.isInstance(value)) {
			return true;

		} else if (type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
			return true;

		} else {
			return false;
		}

	}

	/**
	 * Set a bean's primitive properties to these defaults when SQL NULL is
	 * returned. These are the same as the defaults that ResultSet get* methods
	 * return in the event of a NULL column.
	 */
	private static final Map primitiveDefaults = new HashMap();

	static {
		primitiveDefaults.put(Integer.TYPE, new Integer(0));
		primitiveDefaults.put(Short.TYPE, new Short((short) 0));
		primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0));
		primitiveDefaults.put(Float.TYPE, new Float(0));
		primitiveDefaults.put(Double.TYPE, new Double(0));
		primitiveDefaults.put(Long.TYPE, new Long(0));
		primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
		primitiveDefaults.put(Character.TYPE, new Character('\u0000'));
	}
}
